#!/usr/bin/env python3
# Licensed under GPLv3
#
# Simple http server to allow user control of n2n edge nodes
#
# Currently only for demonstration
# - needs nicer looking html written
# - needs more json interfaces in edge
#
# Try it out with
#   http://localhost:8080/
#   http://localhost:8080/edge/edges
#   http://localhost:8080/edge/supernodes

import argparse
import socket
import json
import socketserver
import http.server
import signal
import functools
import base64

from http import HTTPStatus

import os
import sys
import importlib.machinery
import importlib.util


def import_filename(modulename, filename):
    # look in the same dir as this script
    pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                            filename)
    loader = importlib.machinery.SourceFileLoader(modulename, pathname)
    spec = importlib.util.spec_from_loader(modulename, loader)
    module = importlib.util.module_from_spec(spec)

    try:
        loader.exec_module(module)
    except FileNotFoundError:
        print("Script {} not found".format(pathname), file=sys.stderr)
        sys.exit(1)
    return module


# We share the implementation of the RPC class with the n2n-ctl script. We
# cannot just import the module as 'n2n-ctl' has a dash in its name :-(
JsonUDP = import_filename('n2nctl', 'n2n-ctl').JsonUDP


pages = {
    "/script.js": {
        "content_type": "text/javascript",
        "content": """
var verbose=-1;

function rows2verbose(id, unused, data) {
    row0 = data[0]
    verbose = row0['traceLevel']

    let div = document.getElementById(id);
    div.innerHTML=verbose
}

function rows2keyvalue(id, keys, data) {
    let s = "<table border=1 cellspacing=0>"
    data.forEach((row) => {
        keys.forEach((key) => {
            if (key in row) {
                s += "<tr><th>" + key + "<td>" + row[key];
            }
        });
    });

    s += "</table>"
    let div = document.getElementById(id);
    div.innerHTML=s
}

function rows2keyvalueall(id, unused, data) {
    let s = "<table border=1 cellspacing=0>"
    data.forEach((row) => {
        Object.keys(row).forEach((key) => {
            s += "<tr><th>" + key + "<td>" + row[key];
        });
    });

    s += "</table>"
    let div = document.getElementById(id);
    div.innerHTML=s
}

function rows2table(id, columns, data) {
    let s = "<table border=1 cellspacing=0>"
    s += "<tr>"
    columns.forEach((col) => {
        s += "<th>" + col
    });
    data.forEach((row) => {
        s += "<tr>"
        columns.forEach((col) => {
            val = row[col]
            if (typeof val === "undefined") {
                val = ''
            }
            s += "<td>" + val
        });
    });

    s += "</table>"
    let div = document.getElementById(id);
    div.innerHTML=s
}

function do_get(url, id, handler, handler_param) {
    fetch(url)
      .then(function (response) {
        if (!response.ok) {
            throw new Error('Fetch got ' + response.status)
        }
        return response.json();
      })
      .then(function (data) {
        handler(id,handler_param,data);

        // update the timestamp on success
        let now = Math.round(new Date().getTime() / 1000);
        let time = document.getElementById('time');
        time.innerHTML=now;
      })
      .catch(function (err) {
        console.log('error: ' + err);
      });
}

function do_post(url, body, id, handler, handler_param) {
    fetch(url, {method:'POST', body: body})
      .then(function (response) {
        if (!response.ok) {
            throw new Error('Fetch got ' + response.status)
        }
        return response.json();
      })
      .then(function (data) {
        handler(id,handler_param,data);
      })
      .catch(function (err) {
        console.log('error: ' + err);
      });
}

function do_stop(tracelevel) {
    // FIXME: uses global in script library
    fetch(nodetype + '/stop', {method:'POST'})
}

function setverbose(tracelevel) {
    if (tracelevel < 0) {
        tracelevel = 0;
    }
    // FIXME: uses global in script library
    do_post(
        nodetype + '/verbose', tracelevel, 'verbose',
        rows2verbose, null
    );
}

function refresh_setup(interval) {
    var timer = setInterval(refresh_job, interval);
}
""",
    },
    "/": {
        "content_type": "text/html; charset=utf-8",
        "content": """
<html>
<head>
 <title>n2n edge management</title>
</head>
<body>
 <table>
    <tr>
        <td>Last Updated:
        <td><div id="time"></div>
        <td><button onclick=refresh_job()>update</button>
        <td><button onclick=do_stop()>stop edge</button>
    <tr>
        <td>Logging Verbosity:
        <td>
            <div id="verbose"></div>
        <td>
            <button onclick=setverbose(verbose+1)>+</button>
            <button onclick=setverbose(verbose-1)>-</button>
 </table>
 <br>
 <div id="communities"></div>
 <br>
 Edges/Peers:
 <div id="edges"></div>
 <br>
 Supernodes:
 <div id="supernodes"></div>
 <br>
 <div id="timestamps"></div>
 <br>
 <div id="packetstats"></div>

 <script src="script.js"></script>
 <script>
// FIXME: hacky global
var nodetype="edge";

function refresh_job() {
    do_get(
        nodetype + '/verbose', 'verbose',
        rows2verbose, null
    );
    do_get(
        nodetype + '/communities', 'communities',
        rows2keyvalue, ['community']
    );
    do_get(
        nodetype + '/supernodes', 'supernodes',
        rows2table, ['version','current','macaddr','sockaddr','uptime']
    );
    do_get(
        nodetype + '/edges', 'edges',
        rows2table, ['mode','ip4addr','macaddr','sockaddr','desc']
    );
    do_get(
        nodetype + '/timestamps', 'timestamps',
        rows2keyvalueall, null
    );
    do_get(
        nodetype + '/packetstats', 'packetstats',
        rows2table, ['type','tx_pkt','rx_pkt']
    );
}

refresh_setup(10000);
refresh_job();
 </script>
</body>
</html>
""",
    },
    "/supernode.html": {
        "content_type": "text/html; charset=utf-8",
        "content": """
<html>
<head>
 <title>n2n supernode management</title>
</head>
<body>
 <table>
    <tr>
        <td>Last Updated:
        <td><div id="time"></div>
        <td><button onclick=refresh_job()>update</button>
        <td><button onclick=do_stop()>stop supernode</button>
    <tr>
        <td>Logging Verbosity:
        <td>
            <div id="verbose"></div>
        <td>
            <button onclick=setverbose(verbose+1)>+</button>
            <button onclick=setverbose(verbose-1)>-</button>
        <td><button onclick=do_reload()>reload communities</button>
 </table>
 <br>
 Communities:
 <div id="communities"></div>
 <br>
 Edges/Peers:
 <div id="edges"></div>
 <br>
 <div id="timestamps"></div>
 <br>
 <div id="packetstats"></div>

 <script src="script.js"></script>
 <script>
// FIXME: hacky global
var nodetype="supernode";

function do_reload() {
    fetch(nodetype + '/reload_communities', {method:'POST'})
}

function refresh_job() {
    do_get(
        nodetype + '/verbose', 'verbose',
        rows2verbose, null
    );
    do_get(
        nodetype + '/communities', 'communities',
        rows2table, ['community','ip4addr','is_federation','purgeable']
    );
    do_get(
        nodetype + '/edges', 'edges',
        rows2table,
        ['community','ip4addr','macaddr','sockaddr','proto','desc']
    );
    do_get(
        nodetype + '/timestamps', 'timestamps',
        rows2keyvalueall, null
    );
    do_get(
        nodetype + '/packetstats', 'packetstats',
        rows2table, ['type','tx_pkt','rx_pkt']
    );
}

refresh_setup(10000);
refresh_job();
 </script>
</body>
</html>
""",
    },
}


class SimpleHandler(http.server.BaseHTTPRequestHandler):

    def __init__(self, rpc, snrpc, *args, **kwargs):
        self.rpc = rpc
        self.snrpc = snrpc
        super().__init__(*args, **kwargs)

    def log_request(self, code='-', size='-'):
        # Dont spam the output
        pass

    def _simplereply(self, number, message):
        self.send_response(number)
        self.end_headers()
        self.wfile.write(message.encode('utf8'))

    def _replyjson(self, data):
        self.send_response(HTTPStatus.OK)
        self.send_header('Content-type', 'application/json')
        self.end_headers()
        self.wfile.write(json.dumps(data).encode('utf8'))

    def _replyunauth(self):
        self.send_response(HTTPStatus.UNAUTHORIZED)
        self.send_header('WWW-Authenticate', 'Basic realm="n2n"')
        self.end_headers()

    def _extractauth(self, rpc):
        # Avoid caching the key inside the object for all clients
        rpc.key = None

        header = self.headers.get('Authorization')
        if header is not None:
            authtype, encoded = header.split(' ')
            if authtype == 'Basic':
                user, key = base64.b64decode(encoded).decode('utf8').split(':')
                rpc.key = key

        if rpc.key is None:
            rpc.key = rpc.defaultkey

    def _rpc(self, method, cmdline):
        try:
            data = method(cmdline)
        except ValueError as e:
            if str(e) == "Error: badauth":
                self._replyunauth()
                return

            self._simplereply(HTTPStatus.BAD_REQUEST, 'Bad Command')
            return
        except socket.timeout as e:
            self._simplereply(HTTPStatus.REQUEST_TIMEOUT, str(e))
            return

        self._replyjson(data)
        return

    def _rpc_read(self, rpc):
        self._extractauth(rpc)
        tail = self.path.split('/')
        cmd = tail[2]
        # if reads ever need args, could use more of the tail

        self._rpc(rpc.read, cmd)

    def _rpc_write(self, rpc):
        self._extractauth(rpc)
        content_length = int(self.headers['Content-Length'])
        post_data = self.rfile.read(content_length).decode('utf8')

        tail = self.path.split('/')
        cmd = tail[2]
        cmdline = cmd + ' ' + post_data

        self._rpc(rpc.write, cmdline)

    def do_GET(self):
        if self.path.startswith("/edge/"):
            self._rpc_read(self.rpc)
            return

        if self.path.startswith("/supernode/"):
            self._rpc_read(self.snrpc)
            return

        if self.path in pages:
            page = pages[self.path]

            self.send_response(HTTPStatus.OK)
            self.send_header('Content-type', page['content_type'])
            self.end_headers()
            self.wfile.write(page['content'].encode('utf8'))
            return

        self._simplereply(HTTPStatus.NOT_FOUND, 'Not Found')
        return

    def do_POST(self):
        if self.path.startswith("/edge/"):
            self._rpc_write(self.rpc)
            return

        if self.path.startswith("/supernode/"):
            self._rpc_write(self.snrpc)
            return


def main():
    ap = argparse.ArgumentParser(
            description='Control the running local n2n edge via http')
    ap.add_argument('-t', '--mgmtport', action='store', default=5644,
                    help='Management Port (default=5644)', type=int)
    ap.add_argument('--snmgmtport', action='store', default=5645,
                    help='Supernode Management Port (default=5645)', type=int)
    ap.add_argument('-k', '--key', action='store',
                    help='Password for mgmt commands')
    ap.add_argument('-d', '--debug', action='store_true',
                    help='Also show raw internal data')
    ap.add_argument('port', action='store',
                    default=8080, type=int, nargs='?',
                    help='Serve requests on TCP port (default 8080)')

    args = ap.parse_args()

    rpc = JsonUDP(args.mgmtport)
    rpc.debug = args.debug
    rpc.defaultkey = args.key

    snrpc = JsonUDP(args.snmgmtport)
    snrpc.debug = args.debug
    snrpc.defaultkey = args.key

    signal.signal(signal.SIGPIPE, signal.SIG_DFL)

    socketserver.TCPServer.allow_reuse_address = True
    handler = functools.partial(SimpleHandler, rpc, snrpc)

    httpd = socketserver.TCPServer(("", args.port), handler)
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        return


if __name__ == '__main__':
    main()
